{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Representing a scalar\n",
    "\n",
    "You can construct and manipulate a population of neurons (ensemble) in nengo. This model shows shows how the activity of neural populations can be thought of as representing a mathematical variable (a scalar value). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# Setup the environment\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "import nengo\n",
    "from nengo.dists import Uniform\n",
    "from nengo.utils.matplotlib import rasterplot\n",
    "from nengo.utils.ensemble import tuning_curves"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create the Model\n",
    "\n",
    "This model has paramenters as described in the book and uses a single population (ensemble) of 100 LIF neurons. Note that the default max rates in Nengo 2.0 are (200, 400), so you have to explicitly specify them to be (100, 200) to create the model with the same parameters as described in the book. Moreover the 'Node Factory' feature of ensembles mentioned in the book maps to the 'neuron_type' in Nengo 2.0 which is set to LIF by default. The default values of tauRC, tauRef, radius and intercepts in Nengo 2.0 are the same as those mentioned in the book."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "model = nengo.Network(label='Many Neurons')\n",
    "with model:\n",
    "    # Input\n",
    "    input = nengo.Node(lambda t: np.sin(16 * t))      # Input sine wave with range 1\n",
    "    # input = nengo.Node(lambda t: 4*np.sin(16 * t))   # Input sine wave with range increased to 4\n",
    "    \n",
    "    # Ensemble with 100 LIF neurons  \n",
    "    x = nengo.Ensemble(100, dimensions=1, max_rates = Uniform(100, 200))\n",
    "    \n",
    "    # Connecting input to ensemble\n",
    "    nengo.Connection(input, x) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Run the model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Import the nengo_gui visualizer to run and visualize the model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from nengo_gui.ipython import IPythonViz\n",
    "IPythonViz(model, \"ch2-scalars.py.cfg\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Press the play button in the visualizer to run the simulation. You should see the graphs as shown in the figure below.\n",
    "\n",
    "The graph on the top left shows the input and the graph on the top right shows the the decoded value of the neural spiking (a linearly decoded estimate of the input). The graph on the bottom right shows the spike raster which is the spiking output of the neuron population (x)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from IPython.display import Image\n",
    "Image(filename='ch2-scalars.png')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Increasing the range of Input"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You have seen that the population of neurons does a reasonably good job of representing the input. However, neurons cannot represent arbitrary values well and you can verify this by increasing the range of the input to 4 ( input = nengo.Node(lambda t: 4*np.sin(16 * t)) ). You will observe the same saturation effects as described in the book, showing that the neurons do a much better job at representing information within the defined radius."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Plotting the Tuninig Curves"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The tuning curve of a neurons tells us how it responds to an incoming input signal. Looking at the tuning curves of the neurons in an ensemble is one of the most common ways to debug failures in a model. \n",
    "\n",
    "For a one-dimensional ensemble, since the input is a scalar, we can use the input as x-axis and the neuron response as y-axis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# Alternative way to run the model\n",
    "with nengo.Simulator(model) as sim: # Create the simulator\n",
    "    sim.run(1)                      # Run the simulation for 1 second"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "# Plot the tuning curves of the ensemble\n",
    "plt.figure()\n",
    "plt.plot(*tuning_curves(x, sim))\n",
    "plt.ylabel(\"Firing rate (Hz)\")\n",
    "plt.xlabel(\"Input scalar, x\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If there is some biological or functional reason to impose some pattern to the neuron responses, you can do so by changing the parameters of the ensemble."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from nengo.dists import Choice\n",
    "x.intercepts = Choice([-0.2])  # change the intercept of all neurons -0.2\n",
    "\n",
    "with nengo.Simulator(model) as sim:\n",
    "    plt.plot(*tuning_curves(x, sim))\n",
    "plt.ylabel(\"Firing rate (Hz)\")\n",
    "plt.xlabel(\"Input scalar, x\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that in the above figure, some neurons start firing at -0.2, while others stop firing at 0.2. This is because the input signal, x, is multipled by a neuron's encoder when it is converted to input current. You can also constrain the tuning curves by changing the encoders of the ensemble."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "x.encoders = Choice([[1]])  # change the encoder of all neurons to [1]\n",
    "\n",
    "with nengo.Simulator(model) as sim:\n",
    "    plt.plot(*tuning_curves(x, sim))\n",
    "plt.plot(*tuning_curves(x, sim))\n",
    "plt.ylabel(\"Firing rate (Hz)\")\n",
    "plt.xlabel(\"Input scalar, x\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This gives us an ensemble of neurons that respond very predictably to input. In some cases, this is important for the proper functioning of a model, or to match what we know about the physiology of a brain area or neuron type."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 2",
   "language": "python",
   "name": "python2"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.10"
  },
  "widgets": {
   "state": {},
   "version": "1.1.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
